import './style.scss'; import '@/app/styles/stock-common.scss'; import { cache } from 'react'; import { Metadata } from 'next'; import Link from 'next/link'; import { notFound } from 'next/navigation'; import { fetchStockDetail, fetchStockHistory } from '@/lib/api/stock'; import { changeDirection, changeSymbol, formatChangeRate, formatKrwCompact, formatNumber, tradingStatusLabel } from '@/lib/utils/stock'; import Pager from '../_components/Pager'; const TABS = [ { value: 'price', label: '시세' }, { value: 'news', label: '뉴스' }, { value: 'disclosure', label: '공시' }, { value: 'discussion', label: '토론' } ]; const HISTORY_PER_PAGE = 30; type Props = { params: Promise<{ code: string; }>; searchParams: Promise<{ tab?: string; page?: string; }>; }; // generateMetadata + 본문 이중 호출 방지 (요청 단위 dedupe) const getDetail = cache(async (code: string) => { return await fetchStockDetail(code); }); export async function generateMetadata({ params }: Props): Promise { const { code } = await params; const res = await getDetail(code); if (!res.success || !res.data) { return { title: '종목 상세 — 개미투자' }; } return { title: `${res.data.name}(${res.data.code}) 주가·시세 — 개미투자`, description: `${res.data.name}(${res.data.code}) 전일 종가·등락률·거래량·일별 시세 — 전일 종가 기준(T+1)` }; } export default async function StockDetailPage({ params, searchParams }: Props) { const { code } = await params; const query = await searchParams; const tab = TABS.some(c => c.value === query.tab) ? (query.tab as string) : 'price'; const page = Math.max(Number(query.page) || 1, 1); const res = await getDetail(code); // 없는 코드(형식 불일치 포함)는 404 if (!res.success && res.status === 404) { return notFound(); } // 백엔드 미기동 등 그 외 실패 — 에러 UI 로 우아하게 처리 if (!res.success || !res.data) { return (
종목 정보를 불러오지 못했습니다. 잠시 후 다시 시도해 주세요.
); } const detail = res.data; const latest = detail.recentPrices.length > 0 ? detail.recentPrices[0] : null; const direction = changeDirection(detail.changeRate); const statusLabel = tradingStatusLabel(detail.tradingStatus); // 시세 탭만 히스토리 조회 (request-time) const history = tab === 'price' ? await fetchStockHistory(code, { page, perPage: HISTORY_PER_PAGE }) : null; return (
{/* 상단 요약 — T+1 데이터임을 명시 */}

{detail.name}

{detail.code} {detail.market} {statusLabel && ( {statusLabel} )} {detail.sectorName && {detail.sectorName}}
{formatNumber(detail.closePrice)} {changeSymbol(direction)} {latest ? formatNumber(Math.abs(latest.changeAmount)) : '—'} ({formatChangeRate(detail.changeRate)})
거래량
{formatNumber(latest?.volume)}
거래대금
{formatKrwCompact(latest?.tradingValue)}
시가총액
{formatKrwCompact(detail.marketCap)}

전일 기준 · {detail.lastTradingDate ?? '데이터 준비 중'} 종가 (T+1 반영)

{/* 탭 */}
{tab !== 'price' ? (

준비 중입니다.

) : !history || !history.success || !history.data ? (
일별 시세를 불러오지 못했습니다. 잠시 후 다시 시도해 주세요.
) : history.data.list.length === 0 ? (

수집된 일별 시세가 없습니다.

) : ( <> {history.data.list.map(row => { const rowDirection = changeDirection(row.changeRate); return ( ); })}
{detail.name} 일별 시세 — 날짜, 종가, 등락, 시가, 고가, 저가, 거래량, 거래대금
날짜 종가 등락률 시가 고가 저가 거래량 거래대금
{row.tradingDate} {formatNumber(row.close)} {changeSymbol(rowDirection)} {formatChangeRate(row.changeRate)} {formatNumber(row.open)} {formatNumber(row.high)} {formatNumber(row.low)} {formatNumber(row.volume)} {formatKrwCompact(row.tradingValue)}
)}
); }